Skip to content

淺談處置模式模式和 using 陳述式

TLDR

  • 非託管資源(如資料庫連線、檔案)需透過實作 IDisposable 介面或定義完成項(Finalizer)手動釋放。
  • 實作處置模式(Dispose Pattern)時,應優先使用 Dispose() 方法,並透過 GC.SuppressFinalize(this) 阻止垃圾回收器重複呼叫完成項。
  • IAsyncDisposable 用於非同步釋放資源,建議同時實作 IDisposable 以維持向後相容性。
  • using 陳述式本質上是 try...finally 的語法糖,確保物件在離開作用域時能正確呼叫 Dispose()
  • C# 8.0 引入的 using 宣告語法可減少巢狀縮排,提升程式碼可讀性。

處置模式 (Dispose Pattern)

非託管資源的釋放

什麼情況下會遇到這個問題:當開發者使用如資料庫連線、檔案存取等非託管資源(Unmanaged Resources)時,由於 CLR 的垃圾回收器(GC)無法自動管理這些資源,必須手動釋放。

釋放非託管資源主要有兩種方式:

  • 實作 IDisposable 介面:在 Dispose() 方法中手動釋放資源。
  • 宣告完成項 (Finalizer):在 GC 回收物件時自動呼叫,但應優先使用 Dispose() 方法。

實作範例

實作處置模式時,建議使用以下標準模式以確保資源被正確釋放且不會重複執行:

csharp
public class ResourceHandle : IDisposable {
    private bool disposed = false;
    private IntPtr unmanagedResource;
    private ManagedResource managedResource;

    public ResourceHandle() {
        unmanagedResource = IntPtr.Zero;
        managedResource = new ManagedResource();
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (!disposed) {
            if (disposing) {
                managedResource?.Dispose();
                managedResource = null;
            }

            if (unmanagedResource != IntPtr.Zero) {
                FreeUnmanagedResource(unmanagedResource);
                unmanagedResource = IntPtr.Zero;
            }
            disposed = true;
        }
    }

    ~ResourceHandle() {
        Dispose(false);
    }
}

非同步處置模式

什麼情況下會遇到這個問題:當資源的釋放過程涉及非同步操作(如網路請求)時,應使用 .NET Core 3.0 引入的 IAsyncDisposable 介面。

  • 最佳實作建議:同時實作 IDisposableIAsyncDisposable,以確保在不支援非同步釋放的舊有框架中,資源仍能被正確處理。
  • 優先順序:在 ASP.NET Core 的依賴注入(DI)容器中,若物件同時實作兩者,會優先呼叫 DisposeAsync()
csharp
class ExampleConjunctiveDisposable : IDisposable, IAsyncDisposable {
    IDisposable? disposableResource = new MemoryStream();
    IAsyncDisposable? asyncDisposableResource = new MemoryStream();

    public void Dispose() {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync() {
        await DisposeCoreAsync().ConfigureAwait(false);
        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            disposableResource?.Dispose();
            disposableResource = null;
            if (asyncDisposableResource is IDisposable disposable) {
                disposable.Dispose();
                asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeCoreAsync() {
        if (asyncDisposableResource is not null) {
            await asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }
        if (disposableResource is IAsyncDisposable disposable) {
            await disposable.DisposeAsync().ConfigureAwait(false);
        } else {
            disposableResource?.Dispose();
        }
        asyncDisposableResource = null;
        disposableResource = null;
    }
}

using 陳述式

確保釋放資源

什麼情況下會遇到這個問題:為了避免因忘記呼叫 Dispose() 而導致資源洩漏,或在發生異常時無法執行清理邏輯。

編譯器會將 using 陳述式自動轉換為 try...finally 結構,確保無論是否發生異常,資源皆會被釋放。

csharp
// 傳統寫法
using (ResourceHandle handle = new ResourceHandle()) {
    // 執行邏輯
}

// C# 8.0 新語法:離開作用域時自動呼叫 Dispose()
{
    using ResourceHandle handle = new ResourceHandle();
    // 執行邏輯
}

巢狀與非同步處理

針對多個資源的釋放,可簡化巢狀結構:

  • 合併宣告:若型別相同,可合併在同一個 using 中。
  • 非同步釋放:若物件實作 IAsyncDisposable,需使用 await using
csharp
// 合併宣告
using (ResourceHandle handle1 = new ResourceHandle(), handle2 = new ResourceHandle()) {
    // 執行邏輯
}

// 非同步釋放
await using (AsyncDisposableObject resource = new AsyncDisposableObject()) {
    // 執行邏輯
}

異動歷程

    • 初版文件建立。